package edu.buaa.act.anonymous;

import com.graphhopper.matching.GPXFile;
import com.graphhopper.util.DistanceCalc;
import com.graphhopper.util.DistancePlaneProjection;
import com.graphhopper.util.GPXEntry;
import edu.buaa.act.helper.Utils;
import edu.buaa.act.model.GPSPoint;
import edu.buaa.act.model.Trajectory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by T7810 on 2017/4/23.
 */
public class TrajectorySeparator
{
    private static Logger log = LoggerFactory.getLogger(TrajectorySeparator.class);

    /**
     *
     * @param windowSizeMilliSeconds in milliseconds
     * @param absLimit in meter/second
     * @param relLimit in meter/second
     */
    public static List<List<Trajectory>> timeWindowSeparate(
            List<List<GPSPoint>> anonymousOutData,
            int windowSizeMilliSeconds, int absLimit, int relLimit)
    {
        log.debug("input: the training data processed by anonymous program");
        log.debug("separate one car's long trajectory: cut it into small pieces of trajectories, using a time window algorithm.");
        log.debug("deletion window size {} (seconds)", windowSizeMilliSeconds/1000);
        log.debug("deletion window open/start when speed less than {}(meter/second)", absLimit);
        log.debug("deletion window close/finish when speed change larger than {}(meter/second)", relLimit);

        List<List<Trajectory>> result = new ArrayList<>();

        for(List<GPSPoint> traj : anonymousOutData)
        {
            List<Trajectory> oneCarTraj = timeWindowSeparateOneCarTraj(traj, windowSizeMilliSeconds, absLimit, relLimit);
            result.add(oneCarTraj);
        }
        return result;
    }

    public static List<Trajectory> timeWindowSeparateOneCarTraj(
            List<GPSPoint> traj,
            int windowSizeMilliSeconds, int absLimit, int relLimit)
    {
        DistanceCalc distanceCalc = new DistancePlaneProjection();
        List<Trajectory> oneCarTraj = new ArrayList<>();
        Trajectory newTraj = null;
        int windowStartIndex=0;
        int winSize=0;
        int carID = -1;
        boolean started=false; // if a window open
        double lastSpeed = 0;
        for(int i=0; i<traj.size(); i++)
        {
            if(i==0){
                carID = (traj.get(0).getCarID());
                newTraj = new Trajectory();
                newTraj.setCarID(carID);
            }else {
                double preLat = traj.get(i - 1).getLatitude();
                double preLon = traj.get(i - 1).getLongitude();
                long preTime = traj.get(i - 1).getTimestamp();

                GPSPoint curPoint = traj.get(i);
                double latitude = curPoint.getLatitude();
                double longitude = curPoint.getLongitude();
                long curTime = curPoint.getTimestamp();

                double deltaDis = distanceCalc.calcDist(preLat, preLon, latitude, longitude);
                double delTimeInSecond = ((curTime - preTime) / 1000);
                double speed = deltaDis / delTimeInSecond;

                if (!started) {
                    if (speed < absLimit) { // start a new window
                        started = true;
                        windowStartIndex = i;
                        winSize=1;
                    } else { // not start, add as normal
                        newTraj.addGpsEntry(curPoint);
                    }
                } else { // continue or finish window
                    if (speed < absLimit && Math.abs(speed - lastSpeed) < relLimit)
                    {
                        //continue
                        winSize++;
                    }else{
                        GPSPoint winStartPoint = traj.get(windowStartIndex);
                        if(curTime - winStartPoint.getTimestamp()> windowSizeMilliSeconds){
                            // finish(close) current window without adding these points to traj.
                            // start a new Trajectory.
                            if(!newTraj.getGpsEntries().isEmpty()){
                                oneCarTraj.add(newTraj);
                                newTraj = new Trajectory();
                                newTraj.setCarID(carID);
                            }
                        }else {
                            // window size not reach threshold, close window and add points.
                            // continue adding points to current newTraj.
                            for (int j = windowStartIndex; j < windowStartIndex+winSize; j++) {
                                newTraj.addGpsEntry(traj.get(j));
                            }
                        }
                        newTraj.addGpsEntry(curPoint);
                        started=false;
                    }
                }
                lastSpeed = speed;
            }
        }
        if(newTraj!=null && newTraj.getGpsEntries().size()>0){
            oneCarTraj.add(newTraj);
        }
        return oneCarTraj;
    }

    /**
     * separate one car's long trajectory: cut it into small pieces of trajectories, using a time window & max timeout strategy.
     * @param anonymousOutData input
     * @param windowSizeMilliSeconds timeout
     * @param absLimit absolute speed limit to open window (meter per second)
     * @param relLimit relative speed limit to close window(meter per second)
     * @return short trajectories
     */
    public static List<List<Trajectory>> smartSeparate(
            List<List<GPSPoint>> anonymousOutData,
            int windowSizeMilliSeconds, int absLimit, int relLimit)
    {
        log.debug("");
        log.debug("deletion window size {} (seconds), also is the timeout", windowSizeMilliSeconds/1000);
        log.debug("deletion window open/start when speed less than {}(meter/second)", absLimit);
        log.debug("deletion window close/finish when speed change larger than {}(meter/second)", relLimit);

        List<List<Trajectory>> result = new ArrayList<>();

        for(List<GPSPoint> traj : anonymousOutData)
        {
            List<Trajectory> oneCarTraj = timeWindowSeparateOneCarTraj(traj, windowSizeMilliSeconds, absLimit, relLimit);
            result.add(oneCarTraj);
        }
        log.debug("separation done");
        return result;
    }

    public static List<Trajectory> smartSeparateOneCarTraj(
            List<GPSPoint> traj,
            int windowSizeMilliSeconds, int absLimit, int relLimit)
    {
        List<Trajectory> tmp = timeWindowSeparateOneCarTraj(traj, windowSizeMilliSeconds, absLimit, relLimit);
        List<Trajectory> result = new ArrayList<>();
        for(Trajectory t : tmp)
        {
            List<GPXEntry> gpxEntries = t.getGpsEntries();
            if(gpxEntries.size()>=2)
            {
                List<Integer> breakPointIndex = new ArrayList<>();
                breakPointIndex.add(0);
                for (int i = 1; i < gpxEntries.size(); i++)
                {
                    GPXEntry pre = gpxEntries.get(i);
                    GPXEntry cur = gpxEntries.get(i-1);
                    if(cur.getTime()-pre.getTime()>windowSizeMilliSeconds)
                    {
                        breakPointIndex.add(i);
                    }
                }
                breakPointIndex.add(gpxEntries.size());
                if(breakPointIndex.size()<=2)
                {
                    result.add(t);
                }
                else
                {
                    for(int i=1;i<breakPointIndex.size();i++)
                    {
                        int start = breakPointIndex.get(i-1);
                        int end = breakPointIndex.get(i);
                        Trajectory newT = new Trajectory(gpxEntries.subList(start, end));
                        newT.setCarID(t.getCarID());
                        result.add(newT);
                    }
                }
            }
        }
        return result;
    }

    public static List<List<Trajectory>> separate(List<List<GPSPoint>> rawData, long timeIntervalMillisecond)
    {
        log.debug("input: test data (not processed by anonymous program)");
        log.debug("separate one car's long trajectories into small pieces using a fixed timeInterval({} seconds)", timeIntervalMillisecond/1000);

        List<List<Trajectory>> result = new ArrayList<>();

        for(List<GPSPoint> traj : rawData)
        {
            List<Trajectory> oneCarTraj = separateOneCarTraj(traj, timeIntervalMillisecond);
            result.add(oneCarTraj);
        }

//        printBounds(result);
        return result;
    }

    public static List<Trajectory> separateOneCarTraj(List<GPSPoint> traj, long timeIntervalMillisecond)
    {
        List<Trajectory> oneCarTraj = new ArrayList<>();
        Trajectory newTraj = null;
        int carID = -1;
        for(int i=0; i<traj.size(); i++)
        {
            if(i==0){
                carID = (traj.get(0).getCarID());
                newTraj = new Trajectory();
                newTraj.setCarID(carID);
            }else {
                long preTime = traj.get(i - 1).getTimestamp();
                long curTime = traj.get(i).getTimestamp();

                if(curTime - preTime > timeIntervalMillisecond)
                {
                    oneCarTraj.add(newTraj);
                    newTraj = new Trajectory();
                    newTraj.setCarID(carID);
                }
            }
            newTraj.addGpsEntry(traj.get(i));
        }
        if(newTraj!=null && newTraj.getGpsEntries().size()>0){
            oneCarTraj.add(newTraj);
        }
        return oneCarTraj;
    }

    public static List<Trajectory> separateTrajTooLong(Trajectory in)
    {
        List<Trajectory> shortTrajs = new ArrayList<>();
        int len = in.getGpsEntries().size();
        int mid = len/2;

        Trajectory t1 = new Trajectory(in.getGpsEntries().subList(0,mid));
        t1.setSample(in.isSample());
        t1.setCarID(in.getCarID());
        t1.setHistory(in.isHistory());

        Trajectory t2 = new Trajectory(in.getGpsEntries().subList(mid, len));
        t2.setSample(in.isSample());
        t2.setCarID(in.getCarID());
        t2.setHistory(in.isHistory());

        shortTrajs.add(t1);
        shortTrajs.add(t2);

        return shortTrajs;
    }

    public List<Trajectory> convertOneCarTraj(List<GPSPoint> traj)
    {
        List<Trajectory> oneCarTraj = new ArrayList<>();
        Trajectory newTraj;
        newTraj = new Trajectory();
        newTraj.setCarID(traj.get(0).getCarID());
        for(int i=0; i<traj.size(); i++)
        {
            newTraj.addGpsEntry(traj.get(i));
        }
        oneCarTraj.add(newTraj);
        return oneCarTraj;
    }

    public void result2File(List<Trajectory> oneCarTraj, File file) throws IOException
    {
        DistanceCalc distanceCalc = new DistancePlaneProjection();
        BufferedWriter w = new BufferedWriter(new FileWriter(file));
        long timePrefix = -1;
        for(Trajectory traj: oneCarTraj) {
            double preLat = 0, preLon = 0;
            long lastTime=-1;
            List<GPXEntry> pointList = traj.getGpsEntries();
            for(int i=0; i<pointList.size(); i++)
            {
                GPXEntry point = pointList.get(i);
                if(i>0){
                    long timestamp = point.getTime();
                    double deltaDis = distanceCalc.calcDist(preLat, preLon, point.getLat(), point.getLon());
                    double speed = deltaDis / ((timestamp-lastTime)/1000);
                    w.write(String.format("%d,%.3f\n", (timestamp-timePrefix)/1000, speed));
                }else if(timePrefix==-1){
                    timePrefix = point.getTime();
                }
                lastTime = point.getTime();
                preLat = point.getLat();
                preLon = point.getLon();
            }
            w.write(String.format("%d,%.3f\n", (lastTime-timePrefix)/1000+1, -2f));
        }
        w.close();
    }


    private void printBounds(List<List<Trajectory>> result)
    {
        double minLat=Double.MAX_VALUE;
        double minLon=Double.MAX_VALUE;
        double maxLat=Double.MIN_VALUE;
        double maxLon=Double.MIN_VALUE;
        for(List<Trajectory> oneCarTraj : result)
        {
            for(Trajectory traj :oneCarTraj)
            {
                for(GPXEntry p : traj.getGpsEntries())
                {
                    double lat = p.getLat();
                    double lon = p.getLon();
                    if(lat<minLat) minLat = lat;
                    if(lat>maxLat) maxLat = lat;
                    if(lon<minLon) minLon = lon;
                    if(lon>maxLon) maxLon = lon;
                }
            }
        }
        System.out.printf("Lat: %f %f   Lon: %f %f\n", minLat, maxLat, minLon, maxLon);
    }

}
